log-drains.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { IS_PLATFORM, useFlag, useParams } from 'common'
  3. import { ChevronDown } from 'lucide-react'
  4. import { cloneElement, useState } from 'react'
  5. import { toast } from 'sonner'
  6. import {
  7. Alert,
  8. Button,
  9. DropdownMenu,
  10. DropdownMenuContent,
  11. DropdownMenuItem,
  12. DropdownMenuTrigger,
  13. } from 'ui'
  14. import { GenericSkeletonLoader } from 'ui-patterns'
  15. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  16. import { LogDrainDestinationSheetForm } from '@/components/interfaces/LogDrains/LogDrainDestinationSheetForm'
  17. import { LogDrains } from '@/components/interfaces/LogDrains/LogDrains'
  18. import {
  19. LOG_DRAIN_TYPES,
  20. LogDrainType,
  21. } from '@/components/interfaces/LogDrains/LogDrains.constants'
  22. import DefaultLayout from '@/components/layouts/DefaultLayout'
  23. import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
  24. import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout'
  25. import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
  26. import { DocsButton } from '@/components/ui/DocsButton'
  27. import {
  28. LogDrainCreateVariables,
  29. useCreateLogDrainMutation,
  30. } from '@/data/log-drains/create-log-drain-mutation'
  31. import { LogDrainData, useLogDrainsQuery } from '@/data/log-drains/log-drains-query'
  32. import { useUpdateLogDrainMutation } from '@/data/log-drains/update-log-drain-mutation'
  33. import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
  34. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  35. import { DOCS_URL } from '@/lib/constants'
  36. import type { NextPageWithLayout } from '@/types'
  37. const LogDrainsSettings: NextPageWithLayout = () => {
  38. const { can: canManageLogDrains, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
  39. PermissionAction.ANALYTICS_ADMIN_WRITE,
  40. 'logflare'
  41. )
  42. const [open, setOpen] = useState(false)
  43. const { ref } = useParams() as { ref: string }
  44. const [selectedLogDrain, setSelectedLogDrain] = useState<Partial<LogDrainData> | null>(null)
  45. const [isCreateConfirmModalOpen, setIsCreateConfirmModalOpen] = useState(false)
  46. const [pendingLogDrainValues, setPendingLogDrainValues] =
  47. useState<LogDrainCreateVariables | null>(null)
  48. const [mode, setMode] = useState<'create' | 'update'>('create')
  49. const { hasAccess: hasAccessToLogDrains, isLoading: isLoadingEntitlement } =
  50. useCheckEntitlements('log_drains')
  51. const sentryEnabled = useFlag('SentryLogDrain')
  52. const s3Enabled = useFlag('S3logdrain')
  53. const axiomEnabled = useFlag('axiomLogDrain')
  54. const otlpEnabled = useFlag('otlpLogDrain')
  55. const last9Enabled = useFlag('Last9LogDrain')
  56. const syslogEnabled = useFlag('syslogLogDrain')
  57. const { data: logDrains } = useLogDrainsQuery(
  58. { ref },
  59. { enabled: !isLoadingEntitlement && hasAccessToLogDrains }
  60. )
  61. const { mutate: createLogDrain, isPending: createLoading } = useCreateLogDrainMutation({
  62. onSuccess: () => {
  63. toast.success('Log drain destination created')
  64. setIsCreateConfirmModalOpen(false)
  65. setOpen(false)
  66. },
  67. onError: () => {
  68. toast.error('Failed to create log drain')
  69. setIsCreateConfirmModalOpen(false)
  70. setOpen(false)
  71. },
  72. })
  73. const { mutate: updateLogDrain, isPending: updateLoading } = useUpdateLogDrainMutation({
  74. onSuccess: () => {
  75. toast.success('Log drain updated')
  76. setOpen(false)
  77. },
  78. onError: () => {
  79. setOpen(false)
  80. toast.error('Failed to update log drain')
  81. },
  82. })
  83. const isLoading = createLoading || updateLoading
  84. function handleUpdateClick(drain: LogDrainData) {
  85. setSelectedLogDrain(drain)
  86. setMode('update')
  87. setOpen(true)
  88. }
  89. function handleNewClick(src: LogDrainType) {
  90. setSelectedLogDrain({ type: src })
  91. setMode('create')
  92. setOpen(true)
  93. }
  94. const content = (
  95. <ScaffoldSection isFullWidth id="log-drains" className="gap-6">
  96. <ScaffoldContainer className="flex flex-col gap-10" bottomPadding>
  97. <LogDrainDestinationSheetForm
  98. mode={mode}
  99. open={open}
  100. onOpenChange={(v) => {
  101. if (!v) {
  102. setSelectedLogDrain(null)
  103. }
  104. setOpen(v)
  105. }}
  106. defaultValues={{
  107. ...selectedLogDrain,
  108. type: selectedLogDrain?.type ? selectedLogDrain.type : 'webhook',
  109. }}
  110. isLoading={isLoading}
  111. onSubmit={({ name, description, type, ...values }) => {
  112. const logDrainValues = {
  113. name,
  114. description: description || '',
  115. type,
  116. config: values as any, // TODO: fix generated API types from backend
  117. id: selectedLogDrain?.id,
  118. projectRef: ref,
  119. token: selectedLogDrain?.token,
  120. }
  121. if (mode === 'create') {
  122. setPendingLogDrainValues(logDrainValues)
  123. setIsCreateConfirmModalOpen(true)
  124. } else {
  125. if (!logDrainValues.id || !selectedLogDrain?.token) {
  126. throw new Error('Log drain ID and token is required')
  127. } else {
  128. updateLogDrain(logDrainValues)
  129. }
  130. }
  131. }}
  132. />
  133. {isLoadingPermissions ? (
  134. <GenericSkeletonLoader />
  135. ) : !canManageLogDrains ? (
  136. <Alert variant="default">You do not have permission to manage log drains</Alert>
  137. ) : (
  138. <LogDrains onUpdateDrainClick={handleUpdateClick} onNewDrainClick={handleNewClick} />
  139. )}
  140. </ScaffoldContainer>
  141. <ConfirmationModal
  142. confirmLabel="Add destination"
  143. variant="default"
  144. title="Confirm Log Drain Creation"
  145. visible={isCreateConfirmModalOpen}
  146. onConfirm={() => {
  147. if (pendingLogDrainValues) {
  148. createLogDrain(pendingLogDrainValues)
  149. setPendingLogDrainValues(null)
  150. }
  151. setIsCreateConfirmModalOpen(false)
  152. }}
  153. onCancel={() => {
  154. setIsCreateConfirmModalOpen(false)
  155. setPendingLogDrainValues(null)
  156. }}
  157. >
  158. <div className="text-foreground-light text-sm space-y-2">
  159. <p>
  160. You are about to create a new log drain destination:{' '}
  161. <span className="text-foreground">{pendingLogDrainValues?.name}</span>
  162. </p>
  163. {IS_PLATFORM && (
  164. <p>
  165. This will incur an additional <span className="text-foreground">$60 per month</span>{' '}
  166. charge to your subscription.
  167. </p>
  168. )}
  169. <p>Are you sure you want to proceed?</p>
  170. </div>
  171. </ConfirmationModal>
  172. </ScaffoldSection>
  173. )
  174. // [kemal]: Ordinarily <PageLayout /> would be bundled with the getLayout function below, however in this case we need access to some bits for the "Add destination" button to render as part of the in-built page header in <PageLayout />.
  175. if (!isLoadingEntitlement && hasAccessToLogDrains) {
  176. return (
  177. <PageLayout
  178. title="log drains"
  179. subtitle="send your project logs to third party destinations"
  180. primaryActions={
  181. <>
  182. {!(logDrains?.length === 0) && (
  183. <div className="flex items-center">
  184. <Button
  185. disabled={!hasAccessToLogDrains || !canManageLogDrains}
  186. onClick={() => {
  187. setSelectedLogDrain(null)
  188. setMode('create')
  189. setOpen(true)
  190. }}
  191. type="primary"
  192. className="rounded-r-none px-3"
  193. >
  194. Add destination
  195. </Button>
  196. <DropdownMenu>
  197. <DropdownMenuTrigger asChild>
  198. <Button
  199. type="primary"
  200. title="Choose token scope"
  201. className="rounded-l-none px-[4px] py-[5px]"
  202. icon={<ChevronDown />}
  203. />
  204. </DropdownMenuTrigger>
  205. <DropdownMenuContent align="end" side="bottom">
  206. {LOG_DRAIN_TYPES.filter((t) => {
  207. if (t.value === 'sentry') return sentryEnabled
  208. if (t.value === 's3') return s3Enabled
  209. if (t.value === 'axiom') return axiomEnabled
  210. if (t.value === 'otlp') return otlpEnabled
  211. if (t.value === 'last9') return last9Enabled
  212. if (t.value === 'syslog') return syslogEnabled
  213. return true
  214. }).map((drainType) => (
  215. <DropdownMenuItem
  216. key={drainType.value}
  217. onClick={() => handleNewClick(drainType.value)}
  218. >
  219. <div className="flex items-center gap-3">
  220. {cloneElement(drainType.icon, { height: 16, width: 16 })}
  221. <div className="space-y-1">
  222. <p className="block text-foreground">{drainType.name}</p>
  223. {IS_PLATFORM && (
  224. <p className="text-xs text-foreground-lighter">Additional $60</p>
  225. )}
  226. </div>
  227. </div>
  228. </DropdownMenuItem>
  229. ))}
  230. </DropdownMenuContent>
  231. </DropdownMenu>
  232. </div>
  233. )}
  234. </>
  235. }
  236. secondaryActions={<DocsButton href={`${DOCS_URL}/guides/platform/log-drains`} />}
  237. >
  238. {content}
  239. </PageLayout>
  240. )
  241. }
  242. return content
  243. }
  244. LogDrainsSettings.getLayout = (page) => (
  245. <DefaultLayout>
  246. <SettingsLayout title="log drains">{page}</SettingsLayout>
  247. </DefaultLayout>
  248. )
  249. export default LogDrainsSettings